본문으로 건너뛰기

23.08.11

오늘 한 일

  • 카공실록 리뷰 등록 api 연결 완료
  • gloddy 디자인 시스템 적용

😀

프로그라피 랜딩 페이지 작업을 맡기로 했다.

디자이너 운영진 누나가 먼저 작업 제안을 해서 나도 하기로 했다.

아직 뭔가 이루어진게 없어서 확정은 아니지만, 만약 한다면 실제로 서비스에 적용되는 작업이라 더욱 더 재밌을 것 같다. 😀


Button 공통 컴포넌트 만들기

twMerge 사용 시 커스텀 클래스끼리 병합하는 문제

예를 들어 text-whitetext-h1을 병합하면 text-white text-h1이 되어야 하는데, 마지막에 병합된 클래스가 적용되는 문제가 있었다.

다행히 관련 이슈를 찾아서 해결할 수 있었다.

const customTwMerge = extendTailwindMerge({
classGroups: {
'font-size': [
{
text: Object.keys(fontSizes),
},
],
},
});

const cn = (...inputs: ClassValue[]) => {
return customTwMerge(clsx(inputs));
};

이제 clsx와 twMerge를 사용할 때 cn 유틸 함수를 사용해도 된다!

Button 컴포넌트

미리 컬러를 정의해두었기 때문에, 수월하게 진행할 수 있었다.

Button 코드
import cn from '@/utils/cn';

export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
/**
* 버튼의 크기를 설정합니다. small: 48px, medium: 56px (default: medium)
*/
size?: 'small' | 'medium';
/**
* 버튼의 색상을 설정합니다. (default: solid-primary)
*/
variant?:
| 'solid-primary'
| 'solid-default'
| 'solid-secondary'
| 'outline-warning'
| 'solid-warning';
children: React.ReactNode;
}

export default function Button({
size = 'medium',
variant = 'solid-primary',
className,
disabled,
children,
...props
}: ButtonProps) {
return (
<button
className={cn(
'flex items-center justify-center rounded-8 px-24 py-16 text-subtitle-2',
{
'h-56': size === 'medium',
'h-48': size === 'small',
'bg-primary text-sign-white disabled:bg-primary-light': variant === 'solid-primary',
'bg-button text-sign-secondary disabled:bg-sub disabled:text-sign-caption':
variant === 'solid-default',
'bg-brand-color text-sign-brand disabled:text-sign-white': variant === 'solid-secondary',
'border border-warning bg-warning-color text-warning disabled:border-warning-light disabled:bg-white disabled:text-warning-light':
variant === 'outline-warning',
'bg-warning text-sign-white disabled:bg-sub disabled:text-sign-caption':
variant === 'solid-warning',
},
className
)}
disabled={disabled}
{...props}
>
{children}
</button>
);
}

ButtonGroup 컴포넌트

ButtonGroup 컴포넌트는 Button 컴포넌트를 감싸는 역할을 한다.

positionbottom인 경우, 화면 하단에 고정되어야 한다.

그리고 Group 안 버튼이 하나인 경우와 두 개인 경우를 구분해야 했다.

  • 하나인 경우: 버튼이 화면 너비의 100%를 차지해야 한다.
  • 두 개인 경우: 왼쪽 버튼은 회색 버튼, 오른쪽 버튼은 파란색 버튼이고 두 버튼 사이에는 8px의 간격이 있어야 한다. 그리고 파란색 버튼의 flex-grow는 1이다.

🤔사용하는 곳에서 버튼 스타일을 따로 주고 싶지 않은데 어떻게 해야 할까?

나는 이렇게 쓰고 싶었다.

<ButtonGroup>
<Button>건너뛰기</Button>
<Button>확인</Button>
</ButtonGroup>

이렇게 하기 위해서는 children으로 가져온 Button 컴포넌트에 props를 전달해야 한다.

그래서 먼저 가져온 children이 Button 컴포넌트인지 확인하고, Button 컴포넌트라면 props를 전달하도록 했다.

1. 가져온 children이 Button 컴포넌트인지 확인하기

isValidElement를 사용해서 리액트 엘리먼트인지 확인한다.
그다음 name을 사용해서 Button 컴포넌트인지 확인한다.

const validChildren = Children.toArray(children).filter(
child =>
isValidElement(child) &&
(
child.type as {
name: string;
}
).name === 'Button'
) as ReactElement[];

2. Button 컴포넌트라면 props를 전달하기

cloneElement를 사용해서 props를 전달해주어 복사한 엘리먼트를 리턴한다. 이 때, 기존에 있던 className이 유지되도록 cn 유틸 함수를 사용한다.

const renderElements = (elements: ReactElement[]) => {
if (elements.length === 1) {
const props = elements[0].props as ButtonProps;
return cloneElement(elements[0], {
className: cn('w-full', props.className),
});
}

return (
<div className="flex gap-8">
{elements.map((element, index) => {
const props = elements[index].props as ButtonProps;

if (index === 0) {
return cloneElement(element, {
className: cn('flex-shrink-0', props.className),
variant: props.variant ?? 'solid-default',
});
}
return cloneElement(element, { className: cn('w-full', props.className) });
})}
</div>
);
};

최종 코드

이제 ButtonGroup 컴포넌트를 사용할 수 있다..!

하나인 경우

image
<ButtonGroup>
<Button>확인</Button>
</ButtonGroup>

두 개인 경우

image
<ButtonGroup>
<Button>건너뛰기</Button>
<Button>확인</Button>
</ButtonGroup>
ButtonGroup 코드
import cn from '@/utils/cn';
import { Children, type ReactElement, cloneElement, isValidElement } from 'react';

import type { ButtonProps } from './Button';

interface ButtonGroupProps {
/**
* 버튼 그룹의 위치를 설정합니다. (default: bottom)
*/
position?: 'bottom' | 'contents';
children: React.ReactNode;
}

export default function ButtonGroup({ position = 'bottom', children }: ButtonGroupProps) {
const validChildren = Children.toArray(children).filter(
child =>
isValidElement(child) &&
(
child.type as {
name: string;
}
).name === 'Button'
) as ReactElement[];

const renderElements = (elements: ReactElement[]) => {
if (elements.length === 1) {
const props = elements[0].props as ButtonProps;
return cloneElement(elements[0], {
className: cn('w-full', props.className),
});
}

return (
<div className="flex gap-8">
{elements.map((element, index) => {
const props = elements[index].props as ButtonProps;

if (index === 0) {
return cloneElement(element, {
className: cn('flex-shrink-0', props.className),
variant: props.variant ?? 'solid-default',
});
}
return cloneElement(element, { className: cn('w-full', props.className) });
})}
</div>
);
};

return (
<div
className={cn('mx-auto border-t-1 border-divider bg-white p-20 pt-7', {
'fixed inset-x-0 bottom-0 max-w-450': position === 'bottom',
})}
>
{renderElements(validChildren)}
</div>
);
}

내일 할 일

  • 프로그라피 팀 회의
  • 카공실록 개발
  • gloddy 개발